/*    Copyright 2010 Tobias Marschall
 *
 *    This file is part of MoSDi.
 *
 *    MoSDi is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    MoSDi is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with MoSDi.  If not, see <http://www.gnu.org/licenses/>.
 */

package mosdi.util;

import java.util.Arrays;

/** Class able to encode several integer values into one integer. */
public class ProductEncoder {

	private int[] bases;
	private boolean leastSignificantFirst;
	// if leastSignificantFirst:
	//    basesProducts[i] = basesProducts[0] * ... * basesProducts[i] 
	// otherwise:
	//    basesProducts[i] = basesProducts[i] * ... * basesProducts[bases.length-1]
	private int[] basesProducts;
	
	
	/** Constructor.
	 *  @param bases When an array is encoded, array[i] must be in [0..bases[i]-1]. 
	 */
	public ProductEncoder(int... bases) {
		this(true, bases);
	}
	
	/** Constructor.
	 *  @param bases When an array is encoded, array[i] must be in [0..bases[i]-1]. 
	 *  @param leastSignificantFirst If true, array[0] is least significant when encoding array.
	 */
	public ProductEncoder(boolean leastSignificantFirst, int... bases) {
		this.leastSignificantFirst = leastSignificantFirst;
		if (bases.length<1) throw new IllegalArgumentException();
		long product = 1;
		for (int x : bases) {
			if (x<=0) throw new IllegalArgumentException("Values must not be <=0");
			product *= x;
			if (product>Integer.MAX_VALUE) throw new IllegalArgumentException("Product of bases must be < Integer.MAX_VALUE");
		}
		this.bases = Arrays.copyOf(bases, bases.length);
		basesProducts = new int[bases.length];
		if (leastSignificantFirst) {
			basesProducts[0] = bases[0];
			for (int i=1; i<bases.length; ++i) {
				basesProducts[i] = basesProducts[i-1] * bases[i]; 
			}
		} else {
			basesProducts[bases.length-1] = bases[bases.length-1];
			for (int i=bases.length-2; i>=0; --i) {
				basesProducts[i] = basesProducts[i+1] * bases[i]; 
			}
		}
	}

	/** Given values with value[i] in [0..bases[i]-1], returns 
	 *  \sum_{i=0}^{len-1} array[i] * (\prod_{j=0}^{i-1} bases[i]). */
	public int encode(int... values) {
		if (values.length!=bases.length) throw new IllegalArgumentException("Length mismatch");
		int result = 0;
		int n = 1;
		if (leastSignificantFirst) {
			for (int i=0; i<values.length; ++i) {
				if ((values[i]<0) || (values[i]>=bases[i])) throw new IllegalArgumentException();
				result += values[i]*n;
				n *= bases[i];
			}
		} else {
			for (int i=values.length-1; i>=0; --i) {
				if ((values[i]<0) || (values[i]>=bases[i])) throw new IllegalArgumentException();
				result += values[i]*n;
				n *= bases[i];
			}
		}
		return result;
	}
	
	/** Counterpart to encode(). 
	 *  @throws Throws an {@link java.lang.IllegalArgumentException} if value is to small or to large. */
	public int[] decode(int value) {
		if (value<0) throw new IllegalArgumentException("value<0");
		int[] result = new int[bases.length];
		if (leastSignificantFirst) {
			for (int i=0; i<bases.length; ++i) {
				result[i] = value%bases[i];
				value /= bases[i];
			}
		} else {
			for (int i=bases.length-1; i>=0; --i) {
				result[i] = value%bases[i];
				value /= bases[i];
			}
		}
		if (value>0) throw new IllegalArgumentException("Value too large.");
		return result;
	}

	/** Calling <code>decodeComponent(i, value)</code> returns the same result
	 *  as <code>decode(value)[i]</code>, but faster. */
	public int decodeComponent(int component, int value) { 
		if (leastSignificantFirst) {
			value %= basesProducts[component];
			if (component>0) {
				value /= basesProducts[component-1];
			}
		} else {
			value %= basesProducts[component];
			if (component<bases.length-1) {
				value /= basesProducts[component+1];
			}
		}
		return value;
	}

	public int getBasis(int component) {
		return bases[component];
	}
	
	/** Returns the number of different values encodeable with this encoding scheme. */
	public int getValueCount() {
		if (leastSignificantFirst) {
			return basesProducts[basesProducts.length-1];
		} else {
			return basesProducts[0];
		}
	}
	
	public int[] encodeSequence(int[]... sequences) {
		return encodeSequence(false, sequences);
	}

	/**
	 * @param quite If true, encode will not throw an error if -1 encountered in one sequence
	 *              but not in all others. Instead, -1 is returned at each position where at least
	 *              one input sequence contains -1. 
	 */
	public int[] encodeSequence(boolean quite, int[]... sequences) {
		if (sequences.length!=bases.length) throw new IllegalArgumentException("Wrong number of sequences.");
		int length = sequences[0].length;
		for (int[] s : sequences) {
			if (s.length!=length) throw new IllegalArgumentException("Length mismatch.");
		}
		int[] result = new int[length];
		for (int i=0; i<length; ++i) {
			int minusOneCount = 0;
			for (int[] s : sequences) {
				if (s[i]==-1) minusOneCount+=1;
			}
			if ((quite && (minusOneCount>=1)) || (minusOneCount==sequences.length)) {
				result[i] = -1;
				continue;
			}
			int[] tuple = new int[bases.length];
			for (int j=0; j<bases.length; ++j) tuple[j] = sequences[j][i];
			try {
				result[i] = encode(tuple);
			} catch (IllegalArgumentException e) {
				throw new IllegalArgumentException(String.format("Illegal character tuple %s at position %d.",Arrays.toString(tuple),i));
			}
		}
		return result;
	}

	public int[][] decodeSequence(int[] productSequence) {
		int[][] result = new int[bases.length][productSequence.length];
		for (int i=0; i<productSequence.length; ++i) {
			if (productSequence[i]==-1) {
				for (int j=0; j<bases.length; ++j) result[j][i] = -1;
				continue;
			}
			try {
				int[] decoded = decode(productSequence[i]);
				for (int j=0; j<bases.length; ++j) result[j][i] = decoded[j];
			} catch (IllegalArgumentException e) {
				throw new IllegalArgumentException(String.format("Illegal character (%d) at position %d.",productSequence[i],i));
			}
		}
		return result;
	}

}
